package com.googlecode.continuity.plugin.svnbrowser.client;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import com.googlecode.continuity.core.client.CoreGWT;
import com.googlecode.continuity.plugin.gwtext.client.ContentBox;
import com.googlecode.continuity.plugin.svnbrowser.client.message.SVNBrowserMessages;
import com.googlecode.continuity.plugin.svnbrowser.client.wrapper.SVNDirEntryWrapper;
import com.googlecode.continuity.plugin.svnbrowser.client.wrapper.SVNNodeKindWrapper;

/**
 * SVNBrowser
 *
 * @author Peter Schmitt <aragos@aragos.de>
 */
public class SVNBrowser extends ContentBox implements SourcesSVNBrowserEvents {
// ------------------------------ FIELDS ------------------------------

  private final static String PATH_DEFAULT = "";
  private final static String DEFAULT_ROOT_PREFIX = "root";
  private final static String HISTORY_PREFIX = "svn:";

  private List pathStack;
  private SVNBrowserEventListenerCollection listeners;

  private FlexTable content;
  private ReadSVNRPCAsync readSVNRPC;
  private HorizontalPanel pathContainer;
  private SVNRepositoryHandle handle;

  private int nameColumn = 0;
  private int revisionColumn = 1;
  private int authorColumn = 2;
  private int commitMessageColumn = 3;
  private int maxVisiblePathElements = 5;
  private int commitMessageTruncateLength = 30;
  private String rootPrefix = DEFAULT_ROOT_PREFIX;

  private final SVNBrowserMessages messages = (SVNBrowserMessages) GWT.create(SVNBrowserMessages.class);

// --------------------------- CONSTRUCTORS ---------------------------

  public SVNBrowser() {
    // setup browser display
    pathContainer = new HorizontalPanel();
    Label pathLbl = new Label(messages.path());
    pathContainer.add(pathLbl);
    content = new FlexTable();
    contentContainer.setWidget(content);
    frame.insert(pathContainer, frame.getWidgetIndex(contentContainer));
    setText(messages.header());

    // add styles
    addStyleName("svnBrowser");
    content.addStyleName("content");
    pathContainer.addStyleName("pathContainer");
    pathLbl.addStyleName("path");

    // insert header, use spans to fix IE bug
    content.setWidget(0, nameColumn, new HTML("<span>" + messages.nameColumnHeader() + "</span>"));
    content.setWidget(0, revisionColumn, new HTML("<span>" + messages.revisionColumnHeader() + "</span>"));
    content.setWidget(0, authorColumn, new HTML("<span>" + messages.authorColumnHeader() + "</span>"));
    content.setWidget(0, commitMessageColumn, new HTML("<span>" + messages.commitMessageColumnHeader() + "</span>"));
    for (int i = 0; i < 4; i++) {
      content.getFlexCellFormatter().addStyleName(0, i, "contentHeader");
    }

    // add column styles
    content.getColumnFormatter().addStyleName(nameColumn, "nameColumn");
    content.getColumnFormatter().addStyleName(revisionColumn, "revisionColumn");
    content.getColumnFormatter().addStyleName(authorColumn, "authorColumn");
    content.getColumnFormatter().addStyleName(commitMessageColumn, "commitMessageColumn");

    // get rpc
    readSVNRPC = (ReadSVNRPCAsync) CoreGWT.getRPC("ReadSVNRPCAsync");

    // initialize listeners
    listeners = new SVNBrowserEventListenerCollection();
  }

  public SVNBrowser(SVNRepositoryHandle handle) {
    this(handle, PATH_DEFAULT);
  }

  public SVNBrowser(SVNRepositoryHandle handle, String path) {
    this();
    setHandle(handle);

    // start up browser
    setPath(path);
  }

  public void setPath(String path) {
    if (handle == null) {
      throw new UnsupportedOperationException("Before calling setPath() you MUST set the handle on the SVNBrowser.");
    }
    pathStack = new ArrayList();
    descend(path);
    getContentsForCurrentPath();
  }

  public void descend(String directory) {
    while (directory.startsWith("/")) {
      directory = directory.substring(1);
    }
    while (directory.endsWith("/")) {
      directory = directory.substring(0, directory.length() - 1);
    }
    String[] stack;
    if (directory.indexOf("/") > -1) {
      stack = directory.split("/");
    } else {
      stack = new String[]{directory};
    }
    for (int i = 0; i < stack.length; i++) {
      pathStack.add(stack[i]);
    }
  }

  private void getContentsForCurrentPath() {
    setLoading(true);
    String path = getPath();

    readSVNRPC.getDirectoryContents(handle, path, new AsyncCallback() {
      public void onFailure(Throwable throwable) {
        if (isVisible()) {
          clearBrowser();
          setLoading(false);
          try {
            throw throwable;
          } catch (ReadSVNException e) {
            showError(e.getMessage());
          } catch (Throwable t) {
            GWT.getUncaughtExceptionHandler().onUncaughtException(t);
          }
        }
      }

      public void onSuccess(Object object) {
        if (isVisible()) {
          clearBrowser();
          setLoading(false);
          updatePathContainer();
          displayDirectoryContent((List) object);
        }
      }
    });
  }

  public String getPath() {
    return getPathSection(pathStack.size());
  }

  private String getPathSection(int lastIndex) {
    String path = (String) pathStack.get(0);
    for (int i = 1; i < lastIndex; i++) {
      path += pathStack.get(i) + "/";
    }
    return path.substring(0, path.length() - 1);
  }

  private void clearBrowser() {
    while (content.getRowCount() > 1) {
      content.removeRow(1);
    }
  }

  private void setLoading(boolean loading) {
    if (loading) {
      showStatusMessage(messages.loading());
    } else {
      hideStatusMessage();
    }
  }

  private void updatePathContainer() {
    String separator = "/";
    String omitted = "...";

    // clear container
    while (pathContainer.getWidgetCount() > 1) {
      pathContainer.remove(1);
    }

    // add first segment
    Label element;
    String firstSegment = (String) pathStack.get(0);
    if (firstSegment.length() == 0) {
      firstSegment = rootPrefix;
    }
    element = new Label(firstSegment);
    element.addStyleName("firstElement");
    element.addStyleName("link");
    element.addStyleName("element");
    final String entryPath = (String) pathStack.get(0);
    element.addClickListener(new ClickListener() {
      public void onClick(Widget widget) {
        ascend(pathStack.size() - 1);
        getContentsForCurrentPath();
        listeners.fireOnDirectorySelected(entryPath);
      }
    });
    pathContainer.add(element);

    // check and jump if too many segments are in the path
    int next = 1;
    if (pathStack.size() > maxVisiblePathElements) {
      Label sep = new Label(separator);
      sep.addStyleName("separator");
      pathContainer.add(sep);
      Label om = new Label(omitted);
      om.addStyleName("omitted");
      pathContainer.add(om);
      next = pathStack.size() - maxVisiblePathElements;
    }

    // display last segements
    for (int i = next; i < pathStack.size(); i++) {
      Label sep = new Label(separator);
      sep.addStyleName("separator");
      pathContainer.add(sep);
      element = new Label((String) pathStack.get(i));
      element.addStyleName("element");
      element.addStyleName("link");
      final int j = i;
      final String entryPath2 = getPathSection(pathStack.size() - j);
      element.addClickListener(new ClickListener() {
        public void onClick(Widget widget) {
          ascend(pathStack.size() - j - 1);
          getContentsForCurrentPath();
          listeners.fireOnDirectorySelected(entryPath2);
        }
      });
      pathContainer.add(element);
    }
    element.addStyleName("lastElement");
  }

  /**
   * Pops as many directories from the pathStack as specified with step until step is exhausted or only one directory
   * (root) remains.
   *
   * @param steps number of directories to be ascended
   */
  public void ascend(int steps) {
    steps = steps > pathStack.size() - 1 ? pathStack.size() - 1 : steps;
    for (int i = 0; i < steps; i++) {
      pathStack.remove(pathStack.size() - 1);
    }
  }

  private void displayDirectoryContent(List directoryContent) {
    List files = new ArrayList();

    // add "dir up"
    if (pathStack.size() > 1) {
      // todo: replace with hyperlink for history support
      Label name = new Label("..");
      content.setWidget(1, nameColumn, name);
      final String entryPath = getPathSection(pathStack.size() - 1);
      name.addClickListener(new ClickListener() {
        public void onClick(Widget widget) {
          ascend(1);
          getContentsForCurrentPath();
          listeners.fireOnDirectorySelected(entryPath);
        }
      });
      name.addStyleName("name");
      name.addStyleName("link");
      content.getRowFormatter().addStyleName(1, "dirEntry");
    }

    String path = getPath();

    // add directory entries and collect files
    for (int i = 0; i < directoryContent.size(); i++) {
      final SVNDirEntryWrapper entry = (SVNDirEntryWrapper) directoryContent.get(i);
      int kind = entry.getKind();
      if (kind == SVNNodeKindWrapper.FILE) {
        files.add(entry);
      } else if (kind == SVNNodeKindWrapper.DIR) {
        // todo: replace with hyperlink for history support
        Label name = new Label(entry.getName() + "/");
        int rowCount = content.getRowCount();
        content.setWidget(rowCount, nameColumn, name);
        final String entryPath = path + (path.length() > 0 ? "/" : "") + entry.getName();
        name.addClickListener(new ClickListener() {
          public void onClick(Widget widget) {
            descend(entry.getName());
            getContentsForCurrentPath();
            listeners.fireOnDirectorySelected(entryPath);
          }
        });
        name.addStyleName("name");
        name.addStyleName("link");
        Label revision = new Label(Long.toString(entry.getRevision()));
        revision.addStyleName("revision");
        content.setWidget(rowCount, revisionColumn, revision);
        Label author = new Label(entry.getAuthor());
        author.addStyleName("author");
        content.setWidget(rowCount, authorColumn, author);
        // remove line breaks for IE7 
        String commitMessageText = entry.getCommitMessage().replaceAll("\n", " ");
        if (commitMessageText.length() > commitMessageTruncateLength) {
          commitMessageText = commitMessageText.substring(0, commitMessageTruncateLength - 3) + "...";
        }
        Label commitMessage = new Label(commitMessageText);
        commitMessage.addStyleName("commitMessage");
        content.setWidget(rowCount, commitMessageColumn, commitMessage);
        content.getRowFormatter().addStyleName(rowCount, "dirEntry");
      }
    }

    // add file entries
    for (int i = 0; i < files.size(); i++) {
      final SVNDirEntryWrapper entry = (SVNDirEntryWrapper) files.get(i);
      // todo: replace with hyperlink for history support
      Label name = new Label(entry.getName());
      int rowCount = content.getRowCount();
      final String entryPath = path + (path.length() > 0 ? "/" : "") + entry.getName();
      name.addClickListener(new ClickListener() {
        public void onClick(Widget widget) {
          listeners.fireOnFileSelected(entry, entryPath);
        }
      });
      name.addStyleName("name");
      name.addStyleName("link");
      content.setWidget(rowCount, nameColumn, name);
      Label revision = new Label(Long.toString(entry.getRevision()));
      revision.addStyleName("revision");
      content.setWidget(rowCount, revisionColumn, revision);
      Label author = new Label(entry.getAuthor());
      author.addStyleName("author");
      content.setWidget(rowCount, authorColumn, author);
      // remove line breaks for IE7 
      String commitMessageText = entry.getCommitMessage().replaceAll("\n", " ");
      if (commitMessageText.length() > commitMessageTruncateLength) {
        commitMessageText = commitMessageText.substring(0, commitMessageTruncateLength - 3) + "...";
      }
      Label commitMessage = new Label(commitMessageText);
      commitMessage.addStyleName("commitMessage");
      content.setWidget(rowCount, commitMessageColumn, commitMessage);
      content.getRowFormatter().addStyleName(rowCount, "fileEntry");
    }
  }

// --------------------- GETTER / SETTER METHODS ---------------------

  public SVNRepositoryHandle getHandle() {
    return handle;
  }

  public void setHandle(SVNRepositoryHandle handle) {
    this.handle = handle;
  }

  public void setRootPrefix(String rootPrefix) {
    this.rootPrefix = rootPrefix;
  }

// ------------------------ INTERFACE METHODS ------------------------

// --------------------- Interface SourcesSVNBrowserEvents ---------------------

  public void addSVNBrowserEventListener(SVNBrowserEventListener listener) {
    listeners.add(listener);
  }

  public void removeSVNBrowserEventListener(SVNBrowserEventListener listener) {
    listeners.remove(listener);
  }
}
